Python: feat(bedrock): implement native structured output support via Converse API#6052
Python: feat(bedrock): implement native structured output support via Converse API#6052karthik-0306 wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds structured output support for Bedrock Converse by translating response_format into Bedrock’s outputConfig.textFormat=json_schema, and ensures resulting ChatResponse.value is populated (including streaming), with tests covering schema wiring and an unsupported-model error path.
Changes:
- Implement
outputConfiggeneration from Pydantic models or dict-based JSON schemas and attach it to Converse requests. - Plumb
response_formatthrough response processing/stream building soChatResponse.valuecan be parsed. - Add a new pytest suite validating wire shape, strict-schema behavior, streaming parsing, and a ValidationException-to-ValueError mapping.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| python/packages/bedrock/agent_framework_bedrock/_chat_client.py | Adds outputConfig(json_schema) request support, strict schema mutation, and a clearer error for unsupported models. |
| python/packages/bedrock/tests/test_bedrock_structured_output.py | Introduces tests for outputConfig shape, schema encoding, recursive strictness, parsing into .value, streaming, and unsupported-model handling. |
| # "outputConfig" in error_message catches cases where Bedrock explicitly | ||
| # rejects the outputConfig field (unsupported model). Other ValidationExceptions | ||
| # (e.g. malformed schema shape, invalid property values) will not mention | ||
| # "outputConfig" and will bubble up as raw ClientError without being misdiagnosed. | ||
| if error_code == "ValidationException" and ( | ||
| "outputconfig" in error_message.lower() or "outputconfig" in str(e).lower() | ||
| ): | ||
| raise ValueError( | ||
| f"Model '{self.model}' does not support structured output via outputConfig.textFormat. " | ||
| "Check the model's Bedrock Converse outputConfig/textFormat support. " | ||
| f"AWS error Code: {error_code}. AWS error Message: {error_message}" | ||
| ) from e |
There was a problem hiding this comment.
Checked the existing MAF exception hierarchy — there is no UnsupportedFeature-style exception in the codebase. Two options: use the existing ChatClientInvalidRequestException which semantically fits ("the model rejected this request configuration"), or keep ValueError since it's standard Python for bad argument values and is consistent with how other validation errors are surfaced across MAF. Flagging for human reviewer input before making this call — happy to go either direction.
…or check, docs + test
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
giles17
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 86%
✓ Correctness
The implementation is correct and follows the established patterns from other MAF client implementations (Anthropic, OpenAI, Gemini). The response_format is properly threaded through both streaming (_build_response_stream with response_format kwarg) and non-streaming (_process_converse_response passing response_format to ChatResponse constructor) paths. The _set_additional_properties_false walker correctly handles cycles and respects existing additionalProperties dict schemas. The error handling in _invoke_converse properly catches and reclassifies ClientError for unsupported models. No correctness bugs were found.
✓ Security Reliability
The structured output implementation is well-designed from a security/reliability standpoint. Input validation is present for the main type-check case (non-dict, non-BaseModel raises TypeError), deep copy guards against mutating caller schemas, and the recursive schema walker uses cycle detection. The error handling in _invoke_converse correctly accesses botocore ClientError's guaranteed .response attribute and re-raises non-matching exceptions. The one notable reliability gap (ValueError vs library exception hierarchy) is already tracked in the unresolved review thread. I found one additional minor input validation gap at a trust boundary.
✓ Test Coverage
The test file provides solid coverage for the main happy paths (Pydantic model, OpenAI-style dict, streaming, non-streaming, error case). However, there are three meaningful coverage gaps: (1) no test verifying that non-outputConfig ValidationExceptions propagate as ClientError (the 'raise' pass-through path), (2) no test for the dict schema 'Shape B' code path ({"name": ., "schema": ...} without the json_schema wrapper), and (3) no test verifying that copy.deepcopy prevents mutation of the caller's original dict schema.
✗ Design Approach
I found one design-level contract mismatch. The new Bedrock structured-output path narows
response_formatfrom the framework-wideMapping[str, Any] | BaseModel | Nonecontract down to concretedictinputs only, so valid mapping-based schemas that other parts of the framework accept will now fail on this provider.
Automated review by giles17's agents
|
All feedback addressed in commit 5145466 — isinstance(response_format, dict) widened to Mapping with dict() normalization in Shape C, description line updated to match, and 4 new tests added covering the Mapping path, Shape B, deepcopy mutation guard, and ClientError pass-through. |
Motivation and Context
BedrockChatClient was the only chat client in MAF without structured output support. It actively blocked the feature by hardcoding response_format: None in BedrockChatOptions, silently discarding any schema passed by the caller regardless of what the user specified.
AWS Bedrock's Converse API added native structured output support via outputConfig.textFormat (GA February 4, 2026), making this workaround unnecessary. Every other MAF provider client — Anthropic, OpenAI, and Gemini — already supports response_format. This PR brings Bedrock to full parity.
Fixes #5966.
Description
Removed the response_format: None override in BedrockChatOptions so the field flows through from the parent ChatOptions naturally.
Added _prepare_output_config() which translates MAF's response_format (either a Pydantic model class or an OpenAI-style dict schema) into the exact wire format the Converse API requires:
Added _set_additional_properties_false(), a recursive helper that mirrors the identical method in AnthropicChatClient. It walks the full schema tree and sets additionalProperties: false on every object type, as required by AWS for strict schema enforcement. A copy.deepcopy() guards against mutating the caller's original dict schema.
Threaded response_format through _process_converse_response() and _build_response_stream() so MAF's base class machinery handles response parsing and lazily hydrates ChatResponse.value with the validated Pydantic model — no custom JSON parsing logic needed.
Wrapped _invoke_converse() in a try/except that catches botocore.exceptions.ClientError. When AWS returns a ValidationException referencing outputConfig (which happens when a model like Claude 3.x, Nova, or Llama receives the parameter), it is re-raised as a descriptive ValueError naming the model and listing supported alternatives. No silent fallback to unstructured text.
When response_format is not provided, behaviour is identical to before — no outputConfig is sent and no existing functionality is affected.
Contribution Checklist